<?php

/**
 * 邮件解析类
 * 默认使用UTF-8编码输出
 * 使用PHP的imap扩展实现。
 * 包括以下功能支持：
 * 1、支持邮件header信息解析
 * 2、支持邮件附件信息解析，可以在指定的目录生成邮件的附件
 * 3、支持多收件人格式
 * 4、支持邮件列表总数显示
 * 5、支持邮件分页解析
 * 6、支持根据发件人、时间段、特定标题邮件的批量解析
 * 7、支持解析后的删除操作
 * 8、支持根据发件人、时间段、特定标题邮件的指删除操作
 * 9、支持对没有收件人的邮件或退信邮件的屏蔽
 *
 * @author 胖子 phppan#gmail.com   http:\\www.phppan.com
 * 哥学社成员（http://www.blog-brother.com/）
 */
class Mail {

    public $subject;
    public $body;
    public $charset;
    public $from;
    public $fromName;
    public $to;
    public $toName;
    public $attachments;

}

final class Attachment {
    public $filename;
    public $oldname;

    public function __construct($filename = NULl, $oldname = NULL) {
        $this->filename = $filename;
        $this->oldname = $oldname;
    }
}

class DateRange {

    /**
     * @access public
     * @var string
     */
    public $min;
    /**
     * @access public
     * @var string
     */
    public $max;

    public function __construct($min = NULL, $max = NULL) {
        $this->min = $min;
        $this->max = $max;
    }

}

class Paging {

    /**
     * @access public
     * @var integer
     */
    public $startIndex;
    /**
     * @access public
     * @var integer
     */
    public $numberResults;

    public function __construct($startIndex = NULL, $numberResults = NULL) {
        $this->startIndex = $startIndex;
        $this->numberResults = $numberResults;
    }

}

/**
 * 过滤类
 * 包括标题，内容，发送人地址/发送人姓名，收件人地址/收件人名称
 */
class Filter {

    public $subject;
    public $body;
    public $from;
    public $to;

}

/**
 * 选择器类
 * 包括过滤器，时间选择，分页
 */
class Selector {

    public $filters;
    public $dateRange;
    public $paging;

    public function __construct($filters = NULL, $dateRange = NULL, $paging = NULL) {
        $this->filters = $filters;
        $this->dateRange = $dateRange;
        $this->paging = $paging;
    }

}

/**
 * 邮件解析地址的容器类
 * 不能被创建
 */
final class MailBox {

    private function __construct() {
        
    }

    const GMAILIMAP = '{imap.gmail.com:993/imap/ssl}INBOX';
  
}

class MailsParser {

    public $selector;
    /**
     * 是否解析附件
     * @var <type>
     */
    private $_isParseAttach;
    /**
     * 附件解析存放地址
     * @var <type>
     */
    private $_attachPath;
    private $_marubox;
    private $_total = NULl;

    /**
     *
     * @param <type> $username  用户名
     * @param <type> $password  密码
     * @param <type> $mailbox
     */
    function __construct($username, $password, $mailbox) {
        $this->_isParseAttach = FALSE;   //  默认不解析附件
        $this->_marubox = imap_open($mailbox, $username, $password);
        $this->_attachPath = "attachments/";    //  默认为当前路径的attachmetns目录
    }

    public function allowParseAttach() {
        $this->_isParseAttach = TRUE;
    }

    public function setAttachPath($path) {
        $this->_attachPath = $path;
    }

    /**
     * 根据id取邮件信息
     * @param <type> $mid
     * @return <type>
     */
    public function getMailParserByMId($mid) {
        $mailParser = new MailParser($this->_marubox, $mid);

        return $mailParser;
    }

    /**
     * 根据选择器获取邮件列表
     * @param Selector $selector
     * @return <type>
     */
    public function get(Selector $selector) {
        $mainParsers = $this->_getMainParsers($selector);

        /* 处理查询 */
        $mails = array();
        if (is_array($mainParsers)) {
            foreach ($mainParsers as $mailParser) {

                $mailParser->handleHeader();

                $mailParser->handleBody();

                /* 处理附件 */
                if ($this->_isParseAttach === TRUE) {
                    $mailParser->handleAttachment($this->_attachPath);
                }

                $mails[] = $mailParser->getMail();
            }
        }

        return $mails;
    }

    /**
     * 根据选择器删除邮件
     * @param Selector $selector
     */
    public function del(Selector $selector) {
        $mainParsers = $this->_getMainParsers($selector);

        if (is_array($mainParsers)) {
            foreach ($mainParsers as $mailParser) { //  针对每个邮件解析器，调用删除操作
                $mailParser->delete();
            }
        }
    }

    /**
     * 根据选择器获取邮件解析器列表
     * 所有的邮件解析器列表的获取操作都需求通过这个方法。
     * @param Selector $selector
     * @return array
     */
    private function _getMainParsers(Selector $selector) {
        $this->selector = $selector;
        $paging = $this->selector->paging;

        if ($paging == NULL) {
            $paging = new Paging(1, $this->getTotalMails());
            $this->selector->paging = $paging;
        }

        $mailParsers = array();
        $startIndex = $paging->startIndex;
        $endIndex = $paging->startIndex + $paging->numberResults;

        for ($mid = $startIndex; $mid < $endIndex; $mid++) {
            $mailParsers[] = $this->getMailParserByMId($mid);
        }

        return $mailParsers;
    }

    /** * @brief 取得未读邮件数量
     *
     * @return
     */
    function getTotalMails() {
        if ($this->_total) {
            return $this->_total;
        }

        $this->_total = imap_num_msg($this->_marubox);
        return $this->_total;
    }

    /**
     * 关闭imap连接
     * @return
     */
    function closeMailbox() {
        imap_close($this->_marubox, CL_EXPUNGE);
    }

}

/**
 * 单个邮件解析
 */
class MailParser {

    /**
     * 邮件内容
     * @var 
     */
    private $_mail;
    private $_marubox;
    private $_mid;
    private $_charset;
    private $_structure;
    private $_attachPath;
    /**
     * 对没有收件人的邮件或退信邮件的屏蔽支持
     * @var array
     */
    private $_disableMailbox = array('mailer-daemon' => 1, 'postmaster' => 1);

    public function __construct($marubox, $mid, $charset = 'UTF-8') {
        $this->_marubox = $marubox;
        $this->_mid = $mid;
        $this->_charset = $charset;
        $this->_structure = NULL;

        $this->_mail = new Mail();
        $this->_mail->charset = $charset;
    }

    /**
     * 获取当前邮件解析的mail内容
     * @return Mail 
     */
    public function getMail() {
        return $this->_mail;
    }

    /**
     * 获取邮箱的内容结构
     * @return <type>
     */
    private function _getStructure() {
        if ($this->_structure != NULL) {
            return $this->_structure;
        }

        $this->_structure = imap_fetchstructure($this->_marubox, $this->_mid);
        return $this->_structure;
    }

    /**
     * 获取邮件的部分内容
     * @param <type> $partNumber
     * @return <type>
     */
    private function _getPartBody($partNumber = 1) {
        return imap_fetchbody($this->_marubox, $this->_mid, $partNumber);
    }

    /**
     * 处理邮件头信息
     * @return void
     */
    public function handleHeader() {
        $mailHeader = imap_header($this->_marubox, $this->_mid);

        if ($this->_isDisableMailbox($mailHeader->from[0]->mailbox)) {  //  禁用的邮箱
            return;
        }

        if (is_array($mailHeader->from)) {
            foreach ($mailHeader->from as $from) {
                $this->_mail->from[] = strtolower($from->mailbox) . '@' . $from->host;
                $this->_mail->fromName[] = $this->_mimeHeaderDecode($from->personal);
            }
        }

        if (is_array($mailHeader->to)) {
            foreach ($mailHeader->to as $to) {
                $this->_mail->to[] = strtolower($to->mailbox) . '@' . $to->host;
                $this->_mail->toName[] = $this->_mimeHeaderDecode($to->personal);
            }
        }

        $this->_mail->subject = $this->_mimeHeaderDecode($mailHeader->subject);
        $this->_mail->date = date("Y-m-d H:i:s", strtotime($mailHeader->Date));
        $this->_mail->size = $mailHeader->Size;
    }

    /**
     * 处理邮件内容
     * @return void
     */
    public function handleBody() {
        $this->_handleStructure($this->_getStructure(), $type = 'body');
    }

    /**
     * 处理邮件的附件内容
     * @return void
     */
    public function handleAttachment($path) {
        $this->_attachPath = $path;
        $this->_handleStructure($this->_getStructure(), $type = 'attachment');
    }

    /**
     * 邮件结构分析
     * @param <type> $structure
     * @param <type> $type
     * @return <type>
     */
    private function _handleStructure($structure, $type, $partNumber = 1) {
        if (!$structure) {
            return;
        }

        if ($structure->type == 1) {    //  多个邮件结构
            foreach ($structure->parts as $index => $subStructure) {
                $this->_handleStructure($subStructure, $type, $index);
            }
        } else if ($structure->type == 0 || $structure->type == 2) {  // type = 0 表示 TEXT  type=2表示message

            $charset = '';
            foreach ($structure->parameters as $p) {
                if (strtolower($p->attribute) == "charset") {
                    $charset = $p->value;
                    break;
                }
            }

            $text = $this->_getPartBody($partNumber);
            if ($structure->encoding == 3) {
                $text = imap_base64($text);
            }
            if ($structure->encoding == 4) {
                $text = imap_qprint($text);
            }

            if (strtoupper($structure->disposition) == "ATTACHMENT") {
                $this->_saveAttachment($structure, $partNumber);
            } else {
                $this->_mail->body = $this->_charsetConvert($text, $charset, $this->_charset);
            }
        } else if ($type == 'attachment') { // other
            $this->_saveAttachment($structure, $partNumber);
        }
    }

    /**
     * 保存附件
     * @param <type> $structure
     * @param <type> $partNumber
     */
    private function _saveAttachment($structure, $partNumber) {
        $oldname = "";

        $parameters = $structure->parameters;
        if ($parameters) {
            foreach ($parameters as $value) {
                if ((strtoupper($value->attribute)=='NAME') || ( strtoupper($value->attribute)=='FILENAME')) {
                    $oldname = $value->value;
                    break;
                }
            }
        }

        if (empty($oldname)) {
            $parameters = $structure->dparameters;
            if ($parameters) {
                foreach ($parameters as $value) {
                    if ((strtoupper($value->attribute)=='NAME') || ( strtoupper($value->attribute)=='FILENAME')) {
                        $oldname = $value->value;
                        break;
                    }
                }
            }
        }

        $oldname = $this->_mimeHeaderDecode($oldname);
        $message = $this->_getPartBody($partNumber);

        switch ($structure->encoding) {
            case 0: //  0和1做相同的处理
            case 1:
                $message = imap_8bit($message);
                break;
            case 2:
                $message = imap_binary($message);
                break;
            case 3:
                $message = imap_base64($message);
                break;
            case 4:
                $message = quoted_printable_decode($message);
                break;
            default:
                break;
        }

         if (!$this->_checkAttachExt($oldname)) {
            return FALSE;
        }

        $this->_mail->attachments[] = $this->_createAttachment($oldname, $message);
    }

    /**
     * 检测附件后缀规则是否正确
     * 默认对于长度>10 <2的后缀以及以php或php3为后缀的文件名返回FALSE
     * @param <type> $filename
     * @return <type>
     */
    private function _checkAttachExt($filename) {
        $ext = $this->_getFileExt($filename);
        if (strlen($ext) < 2 || strlen($ext) > 10 || $ext == ".php" || $ext == ".php3") {
            return FALSE;
        }

        return TRUE;
    }

    private function _getFileExt($filename) {
        return substr($filename, strrpos($filename, "."));
    }

    /**
     * 生成附件名称，并存储附件内容。
     * @param <type> $filename
     * @param <type> $content
     * @return Attachment
     */
    private function _createAttachment($filename, $content) {
        /* 年份文件夹 */
        $path = $this->_attachPath . date("Y");
        if (!file_exists($path)) {
            mkdir($path, 0755);
        }

        /* 以天为单位的文件夹 */
        $path .= "/" . date("md");
        if (!file_exists($path)) {
            mkdir($path, 0755);
        }

        $newname = $path . "/" . date("YmdHis") . rand() . $this->_getFileExt($filename);

        /* 附件存储 */
        $fp = fopen($newname, "w");
        fwrite($fp, $message);
        fclose($fp);

        return new Attachment($newname, $filename);
    }

    /**
     * 是否禁用的收件
     * @param <type> $mailbox
     * @return Boolean
     */
    private function _isDisableMailbox($mailbox) {
        return isset($this->_disableMailbox[strtolower($mailbox)]);
    }

    /**
     * 删除邮件
     * @return void
     */
    public function delete() {
        imap_delete($this->_marubox, $this->_mid);
        imap_expunge($this->_marubox);
    }

    /**
     * 编码处理
     * @param string $str
     * @return string
     */
    private function _mimeHeaderDecode($str) {
        $arr_tmp = imap_mime_header_decode($str);

        $charset = array();
        $text = "";

        if (is_array($arr_tmp)) {
            foreach ($arr_tmp as $vtmp) {
                $charset[$vtmp->charset] = TRUE;
            }

            foreach ($arr_tmp as $vtmp) {
                $text .= $vtmp->text;
            }
        }

        $mborder = NULL;
        if ($charset) {
            $charset = array_keys($charset);
            $mborder = implode(',', $charset);
        }

        return $this->_charsetConvert($text, $charset[0], $this->_charset, $mborder);
    }

    /**
     * 字符编码转换
     * @param string $str
     * @param string $from
     * @param string $to
     * @param string $mborder
     * @return string
     */
    private function _charsetConvert($str, $from = NULL, $to = NULL, $mborder = NULL) {
        $from = empty($from) ? $to : strtoupper($from);
        $to = empty($to) ? $to : strtoupper($this->charset);

        if ($from == $to || $from == 'DEFAULT') {
            return $str;
        }

        /* 使用iconv转换编码 */
        if (function_exists('iconv') && ! empty($from)) {
            return iconv($from, $to, $str);
        }

        /* 使用mbstring模块的函数转换编码 */
        if (function_exists("mb_convert_encoding")) {
            if (empty($mborder)) {
                $mborder = "HZ,BIG-5,JIS,UTF-8,ISO-8859-1";
            }
            mb_detect_order($mborder);
            return mb_convert_encoding($str, $to);
        }
        return $str;
    }

}
?>
